/**
 * @file main_gl.c
 * @brief OpenGL based GUI for elgalso
 * @author Scott Moyers
 * @date 2010
 */

#if defined(__APPLE__) || defined(MACOSX)
# include <OpenGL/gl.h>
# include <GLUT/glut.h>
#else
# include <GL/gl.h>
# include <GL/glut.h>
#endif

#include "population.h"
#include "dSFMT.h"
#include "dot_writer.h"
#include "config.h"

#include <stdlib.h>
#include <math.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>

int win_size_x = 1000;
int win_size_y = 500;

float xpos = 0.0;
float ypos = 0.0;

#define DEFAULT_SCALE (1.0f)
float scale = DEFAULT_SCALE;
#define scale_clamp() (scale = scale < 0.01 ? 0.01 : (scale > 1000 ? 1000 : scale))

int zoom_mode = 0;
int old_focus_cf = 0;
int target_focus_cf = 0; /* which population member to focus on */
float cur_focus_cf = 0;
float zoom_cf = -1.0;

enum VIEW_MODE {
	BOX = 0,
	COVER_FLOW
};
#define DEFAULT_MODE (BOX)
int view_mode = DEFAULT_MODE;

/* last seen mouse positions */
int lastx = 0;
int lasty = 0;

/* ga data */
struct Population *p = NULL;
extern SelectionFunc selection_functions[SEL_FUNCS];
extern MutationFunc mutation_functions[MUT_FUNCS];
extern CrossoverFunc crossover_functions[CROSS_FUNCS];
dsfmt_t dsfmt;
double max = 0.0;
#define SEED (getpid() * time(0))
/*#define SEED (1234567) */

int running = 0;

void set_view_mode(enum VIEW_MODE v);
void print_controls();

void init_ga(const char * fn, unsigned int max_gates, unsigned int size)
{
	dsfmt_init_gen_rand(&dsfmt, SEED);
	p = new_population();
	p->truth_table = new_truth_table_from_file(fn);
	print_truth_table(stdout, p->truth_table);
	printf("\n");
	p->max_ckt_size = max_gates;
	add_selection_func(p, fittest_selection);
	add_selection_func(p, roulette_tournament_selection);
	add_mutation_funcs(p, mutation_functions, MUT_FUNCS);
	add_crossover_funcs(p, crossover_functions, CROSS_FUNCS);
	generate_population(p, &dsfmt, size);
	evaluate_population(p);
}

void exec_next_generation(void)
{
	double fit = get_fittest(p)->fitness;
	printf("%d: fittest: %g average: %g\n",
	       p->generation, fit , average_fitness(p));
	next_generation(p, &dsfmt);

	if (fit > max) {
		FILE *ostream;
		max = fit;
		ostream = fopen("fittest.dot", "w");
		print_module_dot(ostream, get_fittest(p), 0);
		fclose(ostream);
	}
}

#define COL 13
float color_lut[COL][3] = {
	{0.0, 0.0, 0.0},
	{0.2, 0.2, 0.2},
	{0.5, 0.5, 0.5},
	{1.0, 0.0, 0.0},
	{0.5, 0.5, 0.0},
	{1.0, 1.0, 0.0},
	{0.0, 0.0, 0.5},
	{0.0, 0.0, 1.0},
	{0.0, 0.5, 0.5},
	{0.0, 1.0, 1.0},
	{0.0, 0.5, 0.0},
	{0.0, 1.0, 0.0},
	{1.0, 1.0, 1.0}
};

#define PORT_SIZE (0.05)
void draw_ports(void)
{
	int port = p->truth_table->nins;
	float dpos = 1.0 / ((float)port + 1.0);
	int i;
	glPushMatrix();
	glPushAttrib(GL_CURRENT_BIT);
	glColor3f(1.0, 1.0, 1.0);
	glBegin(GL_QUADS);

	/* inputs */
	for(i = 1; i <= port; i++) {
		glVertex2f(-PORT_SIZE - 0.5, -PORT_SIZE + dpos * i - 0.5);
		glVertex2f(-PORT_SIZE - 0.5,  PORT_SIZE + dpos * i - 0.5);
		glVertex2f( PORT_SIZE - 0.5,  PORT_SIZE + dpos * i - 0.5);
		glVertex2f( PORT_SIZE - 0.5, -PORT_SIZE + dpos * i - 0.5);
	}

	/* outputs */
	port = p->truth_table->nouts;
	dpos = 1.0 / ((float)port + 1.0);
	for(i = 1; i <= port; i++) {
		glVertex2f(-PORT_SIZE + 0.5, -PORT_SIZE + dpos * i - 0.5);
		glVertex2f(-PORT_SIZE + 0.5,  PORT_SIZE + dpos * i - 0.5);
		glVertex2f( PORT_SIZE + 0.5,  PORT_SIZE + dpos * i - 0.5);
		glVertex2f( PORT_SIZE + 0.5, -PORT_SIZE + dpos * i - 0.5);
	}

	glEnd();
	glPopAttrib();
	glPopMatrix();
}
#undef PORT_SIZE

#define GATE_SIZE (0.01)
void draw_gates(struct Module *m)
{
	unsigned int i;
	glPushMatrix();
	glPushAttrib(GL_CURRENT_BIT);
	glColor3f(0.0, 0.0, 0.0);
	glBegin(GL_QUADS);
	for(i = 0; i < m->num_gates; i++) {
		struct Gate *g = &m->circuit[i];
		if (g->gate_type != CONST_ZERO) {
			glVertex2f(g->x - GATE_SIZE - 0.5, g->y - GATE_SIZE - 0.5);
			glVertex2f(g->x - GATE_SIZE - 0.5, g->y + GATE_SIZE - 0.5);
			glVertex2f(g->x + GATE_SIZE - 0.5, g->y + GATE_SIZE - 0.5);
			glVertex2f(g->x + GATE_SIZE - 0.5, g->y - GATE_SIZE - 0.5);
		}
	}
	glEnd();
	glPopAttrib();
	glPopMatrix();
}
#undef GATE_SIZE

void draw_circuit(unsigned int i)
{
	struct Module *m = p->modules[i];
	layout_module(m);
	draw_ports();
	draw_gates(m);
}

void draw_population(void)
{
	unsigned int i, j;
	glPushMatrix();
	glScalef(10.0, 10.0, 1.0);
	glTranslatef(-sqrt(p->size)/2, -sqrt(p->size)/2, 0.0);
	for(i = 0; i < p->size;) {
		for(j = 0; j < sqrt(p->size) && i < p->size; j++) {
			double f = p->modules[i]->fitness;
			glPushMatrix();
			glScalef(f, f, 1.0);
			glColor3f(color_lut[(int)(f*(COL+1))][0],
				  color_lut[(int)(f*(COL+1))][1],
				  color_lut[(int)(f*(COL+1))][2]);
			glBegin(GL_QUADS);
			glVertex2f(-0.5, -0.5);
			glVertex2f(-0.5,  0.5);
			glVertex2f( 0.5,  0.5);
			glVertex2f( 0.5, -0.5);
			glEnd();
			draw_circuit(i);
			glPopMatrix();
			glTranslatef(1.0, 0.0, 0.0);
			i++;
		}
		glTranslatef(-(signed)j, 1.0, 0.0);
	}
	glPopMatrix();
}

#define CF_SPACE (0.1)
#define TARGET_SPACE (0.2)
#define MAX_ANGLE (80)
#define DEPTH_SCALE (20.0)
void draw_population_cf()
{
	int i;
	double f;
	/* draw previous */
	glPushMatrix();
	glTranslatef(-CF_SPACE * (target_focus_cf + 1) - TARGET_SPACE, 0.0, 0.0);
	for(i = 0; i < target_focus_cf; i++) {
		float angle, depth;
		f = p->modules[i]->fitness;
		angle = MAX_ANGLE;
		depth = -fabs(target_focus_cf - i) / DEPTH_SCALE;
		glPushMatrix();
		glTranslatef(0.0, 0.0, depth);
		glRotatef(angle, 0.0, 1.0, 0.0);
		glScalef(f, f, 1.0);
		draw_circuit(i);

		glColor3f(color_lut[(int)(f*(COL+1))][0],
			  color_lut[(int)(f*(COL+1))][1],
			  color_lut[(int)(f*(COL+1))][2]);
		glBegin(GL_QUADS);
		glVertex2f(-0.5, -0.5);
		glVertex2f( 0.5, -0.5);
		glVertex2f( 0.5,  0.5);
		glVertex2f(-0.5,  0.5);
		glEnd();
		glPopMatrix();
		glTranslatef(CF_SPACE, 0.0, 0.0);
	}
	glPopMatrix();

	glPushMatrix();
	glTranslatef(CF_SPACE * ((signed)p->size - target_focus_cf) + TARGET_SPACE, 0.0, 0.0);
	/* draw successors */
	for(i = (signed)p->size - 1; i > target_focus_cf; i--) {
		float angle, depth;
		f = p->modules[i]->fitness;
		angle = MAX_ANGLE;
		depth = -fabs(target_focus_cf - i) / DEPTH_SCALE;
		glPushMatrix();
		glTranslatef(0.0, 0.0, depth);
		glRotatef(-angle, 0.0, 1.0, 0.0);
		glScalef(f, f, 1.0);
		draw_circuit(i);
		glColor3f(color_lut[(int)(f*(COL+1))][0],
			  color_lut[(int)(f*(COL+1))][1],
			  color_lut[(int)(f*(COL+1))][2]);
		glBegin(GL_QUADS);
		glVertex2f(-0.5, -0.5);
		glVertex2f( 0.5, -0.5);
		glVertex2f( 0.5,  0.5);
		glVertex2f(-0.5,  0.5);
		glEnd();
		glPopMatrix();
		glTranslatef(-CF_SPACE, 0.0, 0.0);
	}
	glPopMatrix();

	/* draw focused circuit */
	glPushMatrix();
	f = p->modules[target_focus_cf]->fitness;
	glScalef(f, f, 1.0);
	draw_circuit(i);
	glColor3f(color_lut[(int)(f*(COL+1))][0],
		  color_lut[(int)(f*(COL+1))][1],
		  color_lut[(int)(f*(COL+1))][2]);
	glBegin(GL_QUADS);
	glVertex2f(-0.5, -0.5);
	glVertex2f( 0.5, -0.5);
	glVertex2f( 0.5,  0.5);
	glVertex2f(-0.5,  0.5);
	glEnd();

	glPopMatrix();
}

void idle(void)
{
	exec_next_generation();
	glutPostRedisplay();
}

void draw_centre()
{
	glColor3f(0.3, 0.3, 0.3);
	glBegin(GL_LINES);
	glVertex3f(-100.0, 0.0, 0.0);
	glVertex3f( 100.0, 0.0, 0.0);
	glVertex3f( 0.0,-100.0, 0.0);
	glVertex3f( 0.0, 100.0, 0.0);
	glEnd();
}

void reshape_box(int w, int h)
{
	win_size_y = h;
	win_size_x = w;
	glViewport(0, 0, (GLsizei)w, (GLsizei)h);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(-win_size_x/2.0, win_size_x/2.0, win_size_y/2.0, -win_size_y/2.0,-1, 1);
	glDisable(GL_DEPTH_TEST);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
}

void reshape_cf(int w, int h)
{
	win_size_y = h;
	win_size_x = w;
	glViewport(0, 0, (GLsizei)w, (GLsizei)h);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(60.0, (GLfloat)w/h, 0.1, 1000.0);
	glEnable(GL_DEPTH_TEST);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	gluLookAt(0.0, 0.0, 5.0,
		  0.0, 0.0, 0.0,
		  0.0, 1.0, 0.0);
}

void mouse_box(int button, int state, int x, int y)
{
	(void) x; (void) y;

	if (button == GLUT_LEFT_BUTTON) {
		int mod = glutGetModifiers();
		if (state == GLUT_DOWN) {
			lastx = x;
			lasty = y;
			if (mod == GLUT_ACTIVE_SHIFT) {
				zoom_mode = 1;
			}
		}

		if (state == GLUT_UP) {
			zoom_mode = 0;
		}
	}
}

void mouse_motion_box(int x, int y)
{
	int diffx = x - lastx;
	int diffy = y - lasty;
	lastx = x;
	lasty = y;
	if (zoom_mode == 0) {
		xpos += (float)diffx / scale;
		ypos += (float)diffy / scale;
	} else {
		scale -= scale * (float)diffy / 10;
		scale_clamp();
	}
	glutPostRedisplay();
}

void keyboard_box(unsigned char key, int x, int y)
{
	(void) x; (void) y;
	switch(key) {
	case 27: /* Esc */
		exit(0);
		break;
	case 'n': /* next generation */
		exec_next_generation();
		glutPostRedisplay();
		break;
	case 'c': /* centre screen */
		ypos = xpos = 0.0;
		scale = DEFAULT_SCALE;
		glutPostRedisplay();
		break;
	case ' ': /* start / stop simulation */
		if (running == 1) {
			glutIdleFunc(NULL);
			running = 0;
		} else {
			glutIdleFunc(idle);
			running = 1;
		}
		break;
	case 'p': /* print controls */
		print_controls();
		break;
	default:
		if (key >= '0' && key <= '9') {
			set_view_mode(key - '0');
		}
		break;
	}
}


void display_box(void)
{
	glClear(GL_COLOR_BUFFER_BIT);
	glLoadIdentity();

	draw_centre();
	glScalef(scale, scale, 0.0);
	glTranslatef(xpos, ypos, 0.0); /* move circuits when mouse is dragged */

	draw_population();
	glutSwapBuffers();
}

void mouse_cf(int button, int state, int x, int y)
{
	(void) x; (void) y; (void) button; (void) state;
}

void mouse_motion_cf(int x, int y)
{
	lastx = x;
	lasty = y;
	glutPostRedisplay();
}

void keyboard_cf(unsigned char key, int x, int y)
{
	(void) x; (void) y;
	switch(key) {
	case 27: /* Esc */
		exit(0);
		break;
	case 'n': /* next generation */
		exec_next_generation();
		glutPostRedisplay();
		break;
	case ' ': /* start / stop simulation */
		if (running == 1) {
			glutIdleFunc(NULL);
			running = 0;
		} else {
			glutIdleFunc(idle);
			running = 1;
		}
		break;
	case 'j': /* left */
		if (target_focus_cf != 0) {
			target_focus_cf -= 1;
		}
		glutPostRedisplay();
		break;
	case 'l': /* right */
		if (target_focus_cf != (signed)p->size - 1) {
			target_focus_cf += 1;
		}
		glutPostRedisplay();
		break;
	case 'h': /* full left */
		target_focus_cf = 0;
		glutPostRedisplay();
		break;
	case ';': /* full right */
		target_focus_cf = p->size - 1;
		glutPostRedisplay();
		break;
	case 'i': /* in */
		zoom_cf += 0.1;
		glutPostRedisplay();
		break;
	case 'k': /* out */
		zoom_cf -= 0.1;
		glutPostRedisplay();
		break;
	case 'p': /* print controls */
		print_controls();
		break;
	default:
		if (key >= '0' && key <= '9') {
			set_view_mode(key - '0');
		}
		break;
	}
}

void display_cf(void)
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	glTranslatef(0.0, 0.0, zoom_cf);
	draw_population_cf();
	glutSwapBuffers();
}

void usage(int argc, char *argv[])
{
	(void) argc;
	printf("Usage:\n");
	printf("\t%s config_file.cfg\n", argv[0]);
	exit(1);
}

void print_controls()
{
	printf("CONTROLS:\n");
	printf("\t0      - Switch to BOX mode\n");
	printf("\t1      - Switch to Cover Flow mode\n");
	printf("\tEsc    - Exit\n");
	printf("\tn      - Next generation\n");
	printf("\tspace  - Start/stop simulation\n");
	printf("\tp      - Print these controls\n");
	printf("\n");
	printf("BOX MODE:\n");
	printf("\tc                            - Centre screen\n");
	printf("\tleft click and drag          - Move focus\n");
	printf("\tshift + left click and drag  - Zoom in/out\n");
	printf("\n");
	printf("COVERFLOW MODE:\n");
	printf("\tj      - Left a module\n");
	printf("\tl      - Right a module\n");
	printf("\th      - Goto first module\n");
	printf("\t;      - Goto last module\n");
	printf("\ti      - Zoom in\n");
	printf("\tk      - Zoom out\n");
	printf("\n");
}

int main(int argc, char ** argv)
{
	if (argc != 2) {
		usage(argc, argv);
	}
	print_controls();

	dsfmt_init_gen_rand(&dsfmt, SEED);
	p = new_population();
	if(load_config_file(p, argv[1]) != 0) {
		fprintf(stderr, "Unable to load configuration file %s\n", argv[1]);
		return 1;
	}

	generate_population(p, &dsfmt, p->size);
	evaluate_population(p);

	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH);
	glutInitWindowSize(win_size_x, win_size_y);
	glutCreateWindow("elgalso");

	glClearColor(0.0, 0.0, 0.0, 1.0);
	set_view_mode(DEFAULT_MODE);

	glutMainLoop();

	delete_population(p);
	return 0;
}

void set_view_mode(enum VIEW_MODE v)
{
	switch(v) {
	case BOX:
		view_mode = v;
		glutDisplayFunc(display_box);
		glutReshapeFunc(reshape_box);
		glutKeyboardFunc(keyboard_box);
		glutMotionFunc(mouse_motion_box);
		glutMouseFunc(mouse_box);
		reshape_box(win_size_x, win_size_y);
		glutPostRedisplay();
		printf("View mode: BOX\n");
		break;
	case COVER_FLOW:
		view_mode = v;
		glutDisplayFunc(display_cf);
		glutReshapeFunc(reshape_cf);
		glutKeyboardFunc(keyboard_cf);
		glutMotionFunc(mouse_motion_cf);
		glutMouseFunc(mouse_cf);
		reshape_cf(win_size_x, win_size_y);
		glutPostRedisplay();
		printf("View mode: COVER_FLOW\n");
		break;
	default:
		fprintf(stderr, "Unknown view mode: %d\n", v);
		break;
	}
}
